// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt.tag.html;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import com.wutka.dtd.DTDAny;
import com.wutka.dtd.DTDAttribute;
import com.wutka.dtd.DTDDecl;
import com.wutka.dtd.DTDElement;
import com.wutka.dtd.DTDEmpty;
import com.wutka.dtd.DTDEnumeration;
import com.wutka.dtd.DTDItem;
import com.wutka.dtd.DTDMixed;
import com.wutka.dtd.DTDName;
import com.wutka.dtd.DTDPCData;
import org.owasp.jxt.Encoder;
import org.owasp.jxt.ExprNode;
import org.owasp.jxt.JxtExpr;
import org.owasp.jxt.JxtNode;
import org.owasp.jxt.TextNode;
import org.owasp.jxt.TranslateContext;
import org.owasp.jxt.ValidationContext;

/**
 * AbstractHtmlNode
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
public abstract class AbstractHtmlNode extends JxtNode {

//     private static final Set<String> NOENDTAG = new HashSet<String>(
//         Arrays.asList("br,hr,img,input,link,meta".split(",")));

//     private static final Set<String> CLOSE_TAG_REQD = new HashSet<String>(
//         Arrays.asList(
//             ("tt,i,b,u,s,strike,big,small,em,strong,dfn,code,samp,kbd,var,"+
//              "cite,abbr,acronym,sub,sup,span,bdo,font,address,div,center,a,"+
//              "map,object,applet,h1,h2,h3,h4,h5,h6,pre,q,blockquote,ins,del,"+
//              "dl,dt,dd,ol,dir,menu,ul,form,label,select,optgroup,option,"+
//              "textarea,fieldset,legend,button,table,caption,tr,th,td,"+
//              "frameset,iframe,noframes,title,style,script,noscript")
//             .split(",")));

    /**
     * These are all the attributes that are URIs.  This information
     * is in the DTD as "%URI;", however it's more effort than its
     * worth to have a parser extract that, when a grep will do.
     */
    private static final Set<String> URI_ATTRS = new HashSet<String>(
        Arrays.asList(
            "action,background,cite,classid,codebase,data,href,longdesc,profile,src,usemap"
            .split(",")));


    Map<String,JxtExpr> _attrs = new TreeMap<String,JxtExpr>();

    DTDElement _dtdElement;

    public AbstractHtmlNode(DTDElement element) {
        super(element.name);
        _dtdElement = element;
    }

    /**
     * Returns true if this tag should not have an end tag.
     *
     * @return true if this tag should not have an end tag.
     */
    protected final boolean noEndTag() {
        // return NOENDTAG.contains(_dtdElement.name);
        return _dtdElement.content instanceof DTDEmpty;
    }

    @Override
    public final void setAttribute(ValidationContext ctx, String name, String value) {
        name = name.toLowerCase(); // this is a debatable practice...

        // TODO: warn if was not lower case

        // TODO: check if attribute already set
        _attrs.put(name, toExpr(ctx, value));
    }

    private static final String NAME_START_CHAR =
        ":A-Za-z_"+
        "\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0370-\u037D"+
        "\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF"+
        "\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD";

    private static final String NAME_CHAR =
        "-" +
        NAME_START_CHAR +
        ".0-9\u00B7\u0300-\u036F\u203F-\u2040";

    private static final Pattern NAME = Pattern.compile(
        "["+NAME_START_CHAR+"]["+NAME_CHAR+"]*");

    private static final Pattern NAMES = Pattern.compile(
        NAME+"(?:\\s+"+NAME+")*");

    private static final Pattern NMTOKEN = Pattern.compile(
        "["+NAME_CHAR+"]+");

    @Override
    public final void validateSelf(ValidationContext ctx) {
        // TODO: attr.decl == DTDDecl.FIXED attributes (<pre>, <style>
        // have "xml:space" as #FIXED to 'preserve')

        validateRequiredAttributes(ctx);
        validateAttributeTypes(ctx);
        validateContent(ctx);
    }

    private void validateRequiredAttributes(ValidationContext ctx) {
        // Check for any missing required attributes
        for (Object dtdAttrObj : _dtdElement.attributes.values()) {
            DTDAttribute dtdAttr = (DTDAttribute)dtdAttrObj;
            if (dtdAttr.decl == DTDDecl.REQUIRED &&
                !_attrs.containsKey(dtdAttr.name))
            {
                ctx.warn(this, "validation.attribute-required", dtdAttr.name);
            }
        }
    }

    private void validateAttributeTypes(ValidationContext ctx) {
        // Check the validity of the attributes (type and presense)
        for (Entry<String,JxtExpr> attr : _attrs.entrySet()) {
            String name = attr.getKey();
            DTDAttribute dtdAttr = (DTDAttribute)_dtdElement.attributes.get(name);
            if (dtdAttr == null) {
                ctx.warn(this, "validation.attribute-unknown", name);
                // attribute does not exist in DTD, nothing more to do
                // on this attribute.
                continue;
            }

            JxtExpr value = attr.getValue();

            if (dtdAttr.decl == DTDDecl.FIXED &&
                (!value.isLiteral() ||
                 !dtdAttr.defaultValue.equals(value.getText())))
            {
                ctx.error(this, "validation.attribute-fixed", name, dtdAttr.defaultValue);
                continue;
            }

            if (!value.isLiteral()) {
                // attribtue is an expression, there's nothing we can
                // check now.
                continue;
            }

            String text = value.getText();
            Object type = dtdAttr.type;

            if (type instanceof String) {
                if (!"CDATA".equals(type)) {
                    if ("NMTOKEN".equals(type)) {
                        if (!NMTOKEN.matcher(text).matches()) {
                            ctx.warn(this, "validation.attribute-xml-nmtoken", name);
                        }
                    } else if ("ID".equals(type)) {
                        if (!NAME.matcher(text).matches()) {
                            ctx.warn(this, "validation.attribute-xml-id", name);
                        }
                    } else if ("IDREF".equals(type)) {
                        if (!NAME.matcher(text).matches()) {
                            ctx.warn(this, "validation.attribute-xml-id", name);
                        }
                        // To really validate, this would also need to
                        // verify that the ID exists on the page. (This
                        // would at best be an warning at this stage
                        // though, since the ID could be generated by
                        // code)
                    } else if ("IDREFS".equals(type)) {
                        if (!NAMES.matcher(text).matches()) {
                            ctx.warn(this, "validation.attribute-xml-idrefs", name);
                        }
                    }

                    // ENTITY, ENTITIES, NMTOKENS also possible according
                    // to XML spec, but they are not present in XHTML's
                    // DTD.
                }

            } else if (type instanceof DTDEnumeration) {
                DTDEnumeration valueEnum = (DTDEnumeration)type;
                if (!valueEnum.getItemsVec().contains(text)) {
                    ctx.warn(this, "validation.attribute-invalid", name, valueEnum.getItemsVec());
                }
            }
            // DTD parser could also return a DTDNotationList
            // } else if (dtdAttr.type instanceof DTDNotationList) {
        }
    }

    private static void collectChildren(JxtNode parent, List<JxtNode> children) {
        for (JxtNode c = parent.getFirstChild() ;
             c != null ;
             c = c.getNextSibling())
        {
            if (c instanceof TextNode ||
                c instanceof ExprNode ||
                c instanceof AbstractHtmlNode)
            {
                children.add(c);
            } else {
                collectChildren(c, children);
            }
        }
    }

    private static boolean isValid(DTDItem type, JxtNode node) {
        if (type instanceof DTDPCData) {
            return !(node instanceof AbstractHtmlNode);
        }

        if (type instanceof DTDName) {
            if (node instanceof AbstractHtmlNode) {
                return ((AbstractHtmlNode)node)._dtdElement.name.equals(
                    ((DTDName)type).value);
            } else if (node instanceof TextNode) {
                return ((TextNode)node).getText().trim().length() == 0;
            } else {
                return false;
            }
        }

        if (type instanceof DTDMixed) {
            for (DTDItem item : ((DTDMixed)type).getItems()) {
                if (isValid(item, node)) {
                    return true;
                }
            }

            return false;
        }

        return true;
    }

    private void validateContent(ValidationContext ctx) {
        DTDItem content = _dtdElement.content;

        if (content instanceof DTDEmpty) {
            if (getFirstChild() != null) {
                ctx.error(this, "validation.content-empty", _dtdElement.name);
            }
        } else if (!(content instanceof DTDAny)) {

            // first collect the HTML children of this element
            // (skipping through non-HTML elements)
            List<JxtNode> children = new ArrayList<JxtNode>();
            collectChildren(this, children);

            // Check each node (note: this is only checking mixed
            // content for now)
            for (JxtNode n : children) {
                if (!isValid(content, n)) {
                    if (n instanceof AbstractHtmlNode) {
                        ctx.error(n, "validation.invalid-containment",
                                  ((AbstractHtmlNode)n)._dtdElement.name,
                                  _dtdElement.name);
                    } else {
                        ctx.error(n, "validation.content-no-text", _dtdElement.name);
                    }
                }
            }

            // TODO: DTDChoice and DTDSequence

//             else if (content instanceof DTDChoice) {
//                 // only one of ((DTDChoice)content).items is allowed.  The
//                 // choice may be optional and/or repeated (?*+)
//             } else if (content instanceof DTDSequence) {
//                 // ((DTDSequence)content).items must occur in sequence
//             }
        }
    }

    /**
     * Parses a URI expression into constituent components to apply
     * the context-sensitive encoding within URIs.  Base URIs and
     * complete URIs get escaped with Encoder.URI, where-as URI
     * components (key/value pairs) get escaped with
     * Encoder.URI_COMPONENT.  The output of this method must still
     * be escaped via standard XML attribute escape (mostly to convert
     * the &amp;s to &amp;amp;s and single quotes (') to &amp;#39;
     * since the resulting encoded URI may still have those characters
     * (but notably will not have &lt;, &gt; and ").
     *
     * @param ctx where to write the converted URI attribute
     * @param expr the expression to convert
     */
    static void convertURI(TranslateContext ctx, JxtExpr expr) {
        URIAttributeConverter.convert(ctx, expr);
    }

    @Override
    public final void translate(TranslateContext ctx) {
        ctx.data("<"+_dtdElement.name);

        Iterator<Entry<String,JxtExpr>> attrIter = _attrs.entrySet().iterator();
        while (attrIter.hasNext()) {
            Entry<String,JxtExpr> attr = attrIter.next();
            String name = attr.getKey();
            JxtExpr value = attr.getValue();
            DTDAttribute dtdAttr = (DTDAttribute)_dtdElement.attributes.get(name);

            // TODO:

            // #FIXED attributes...
            //   always write out <html>'s xmlns?
            //   never write out pre's #FIXED attr

            if (URI_ATTRS.contains(name)) {
                // special handling for URI attributes.  Convert them
                // using URI encoding.
                ctx.data(" "+name+"=\"");
                convertURI(ctx, value);
                ctx.data("\"");
            } else if (attr.getValue().isLiteral()) {
                ctx.data(" "+name+"=\""+Encoder.XML.apply(value.getText())+"\"");
            } else if (dtdAttr != null && dtdAttr.decl == DTDDecl.REQUIRED) {
                // #REQUIRED attribute must ALWAYS be written, even if
                //   empty. (e.g. alt="").  We cannot use the
                //   JxtUtil.appendAttribute method here since it will
                //   not write attributes if they are at their default
                //   value.
                ctx.data(" "+name+"=\"");
                ctx.codeln("Encoder.XML.apply(out, "+value+");");
                ctx.data("\"");
            } else {
                // #IMPLIED attributes set to default value (including
                //   null/empty-string) can be removed in most cases (both
                //   at translation time and runtime)

                String defaultValue = (dtdAttr != null) ? dtdAttr.defaultValue : null;

                if (defaultValue == null) {
                    defaultValue = "null";
                } else {
                    defaultValue = "\""+Encoder.JAVA_STRING.apply(defaultValue)+"\"";
                }

                ctx.codeln("JxtUtils.appendAttribute(out, \""+Encoder.JAVA_STRING.apply(name)+"\", "+
                           value+", "+defaultValue+");");
            }
        }

        // TODO: _dtdElement content type

        if (getFirstChild() == null && noEndTag()) {
            ctx.data("/>");
        } else {
            ctx.data(">");
            translateBody(ctx);
            ctx.data("</"+_dtdElement.name+">");
        }
    }

    protected abstract void translateBody(TranslateContext ctx);

} // AbstractHtmlNode
